1.前言
前天的浏览 GitHub 时发现一个模仿 Gif 的 Loading 特效的项目,感觉效果很不错,也比较有创意,如下:
GitHub 上好几个做这个效果的项目,但是很少有完全实现的,有的还有 Bug,于是花了 2 天实现了一下。
效果如下:
GitHub 项目在这里 LeavesLoading
2. 分析
实现要求:
- 叶子
- 随机产生
- 飘动轨迹为正弦函数,并且随机振幅
- 飘动时伴随自旋转,更符合物理规律
- 遇到进度条似乎是融入的
- 风扇
- 可旋转
- Loading == 100% 时显示一个动画
- 细节
- 风扇和叶子自适应 View 大小
- 叶子在视觉上不能飘出 RountRect 边界
3. 核心实现
3.1 随机产生叶子
本质是事先产生一定数量叶子,这些叶子的漂动时的振幅、相位、旋转方向等等都是随机的,并且飘动是周期性地即叶子飘动到最左边时,又重新回到最右边。
Leaf 类:
1 2 3 4 5 6 7 8
| private class Leaf{ float x,y; AmplitudeType type; int rotateAngle; RotateDir rotateDir; long startTime; int n; }
|
Leaf 生成方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| Leaf generateLeaf(){ Leaf leaf = new Leaf(); int randomType = mRandom.nextInt(3); switch (randomType){ case 0: leaf.type = AmplitudeType.LITTLE; break; case 1: leaf.type = AmplitudeType.MIDDLE; break; default: leaf.type = AmplitudeType.BIG; break; } int dir = mRandom.nextInt(2); switch (dir){ case 0: leaf.rotateDir = RotateDir.ANTICLOCKWISE; break; default: leaf.rotateDir = RotateDir.CLOCKWISE; break; } leaf.rotateAngle = mRandom.nextInt(360); leaf.n = mRandom.nextInt(20); mAddTime += mRandom.nextInt((int)mLeafFloatTime); leaf.startTime = System.currentTimeMillis() + mAddTime; return leaf; }
|
3.2 叶子飘动轨迹为正弦函数
确定 Leaf 在某个时刻的坐标 ( x , y ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
private void getLeafLocation(Leaf leaf,long currentTime){ long intervalTime = currentTime - leaf.startTime; if (intervalTime <= 0){ return; }else if (intervalTime > mLeafFloatTime){ leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime); } float fraction = (float) intervalTime / mLeafFloatTime; leaf.x = (1-fraction)*mProgressLen; leaf.y = getLeafLocationY(leaf);
if (leaf.x <= mYellowOvalHeight / 4){ leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime); leaf.x = mProgressLen; leaf.y = getLeafLocationY(leaf); } }
|
要想让 Leaf 飘动轨迹为正弦函数,关键在于确定 Leaf 的 Y 轴坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
private float getLeafLocationY(Leaf leaf){ float w = (float) (Math.PI * 2 / mProgressLen); float A; switch (leaf.type){ case LITTLE: A = mLeafLen/3; break; case MIDDLE: A = mLeafLen*2/3; break; default: A = mLeafLen; break; } return (float) (A * Math.sin(w * leaf.x + leaf.n)+(mHeight-mLeafLen)/2); }
|
3.3 叶子飘动时自旋转
这里就涉及到了 Leaf 的绘制,其实 Gif 中的叶子和风扇都可以使用 Canves 直接绘制图案,但是这样就会有两个问题:
- 难画:想要画出满意图形,并且还要旋转、缩放、平移可要下一番功夫。
- 灵活性低:如果想换其他样式又得重新设计绘制过程。
因此这里采用 Canves.drawBitmap()
的方式绘制,直接使用已有的图片作为叶子和风扇,同时利用 Canves.drawBitmap()
的一个重载的方法可以很方便的实现旋转、缩放、平移:
1
| void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) ;
|
就是通过这里的 Matrix 矩阵,它内部封装了 postScale()
、postTranslate
、postRotate()
等方法,可以帮助我们快速的对 Bitmap 进行旋转、缩放、平移还有其他操作。使用时要记得配合 Canves 的 save()
和 restore()
使用,否则达不到想要的效果。
对这方面不熟的朋友可以看看 HenCoder 的自定义 View 教学 1-4 。
绘制 Leaf 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| private void drawLeaves(Canvas canvas){ long currentTime = System.currentTimeMillis(); for (Leaf leaf : mLeafList) { if (currentTime > leaf.startTime && leaf.startTime != 0){ getLeafLocation(leaf,currentTime); canvas.save(); Matrix matrix = new Matrix(); float scaleX = (float) mLeafLen / mLeafBitmapWidth; float scaleY = (float) mLeafLen / mLeafBitmapHeight; matrix.postScale(scaleX,scaleY); float transX = leaf.x; float transY = leaf.y; matrix.postTranslate(transX,transY); float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime) /(float)mLeafRotateTime; float rotate; switch (leaf.rotateDir){ case CLOCKWISE: rotate = rotateFraction * 360 + leaf.rotateAngle; break; default: rotate = -rotateFraction * 360 + leaf.rotateAngle; break; } matrix.postRotate(rotate,transX + mLeafLen / 2,transY + mLeafLen / 2); canvas.drawBitmap(mLeafBitmap,matrix,mBitmapPaint); canvas.restore(); } }
|
3.4 Loading == 100% 出现动画
增加一个判断字段 isLoadingCompleted ,在 onDraw()
中选择对应绘制策略。
isLoadingCompleted 在 setProgress()
中根据 progress 设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public void setProgress(int progress){ if (progress < 0){ mProgress = 0; }else if (progress > 100){ mProgress = 100; }else { mProgress = progress; } if (progress == 100){ isLoadingCompleted = true; }else { isLoadingCompleted = false; } mCompletedFanPaint.setAlpha(255); postInvalidate(); }
|
LeavesLoading.onDraw()
部分实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); ...... if (isLoadingCompleted){ drawCompleted(canvas); }else { drawFan(canvas,mFanLen,mBitmapPaint); } postInvalidate(); }
|
drawCompleted()
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| private void drawCompleted(Canvas canvas) { int alpha = mCompletedFanPaint.getAlpha() - 10; if (alpha <= 0){ alpha = 0; } mCompletedFanPaint.setAlpha(alpha); mCompletedTextPaint.setAlpha(255-alpha); float fraction = alpha / 255f; float fanLen = fraction * mFanLen; float textSize = (1 - fraction) * mCompletedTextSize; mCompletedTextPaint.setTextSize(textSize); Rect bounds = new Rect(); mCompletedTextPaint.getTextBounds( LOADING_COMPLETED, 0, LOADING_COMPLETED.length(), bounds); drawFan(canvas, (int) fanLen, mCompletedFanPaint); canvas.drawText( LOADING_COMPLETED, 0, LOADING_COMPLETED.length(), mFanCx-bounds.width()/2f, mFanCy+bounds.height()/2f, mCompletedTextPaint); }
|
流程:计算风扇和文字透明度 -> 计算风扇和文字大小以及文字占用空间 -> 绘制 ,注释写得比较清楚就不赘述了。
4. 结束
如果觉得 LeavesLoading 对您有任何帮助,希望可以在 GitHub 得到您的 Star !
Thanks: